Skip to content

feat(spec)!: retire ListViewSchema.navigation.view, the detail-view binding nothing ever resolved - #18619

Merged
os-bill merged 3 commits into
mainfrom
claude/issue-16885-retire-navigation-view
Sep 17, 2026
Merged

os-bill merged 3 commits into
mainfrom
claude/issue-16885-retire-navigation-view

Conversation

@os-bill

@os-bill os-bill commented Sep 17, 2026

Copy link
Copy Markdown
Collaborator

Fixes #16885

Clause-②: no

Retires ListViewSchema.navigation.view under ADR-0049 enforce-or-remove, executing the
maintainer ruling of 2026-09-13 (director decision batch #126 item 4, comment 5651023571,
verbatim 「同意」, option B), with its item 5 superseded by comment 5657440531: the
changeset level is minor, not major.

What the key did

navigation.view was an unconstrained string whose describe promised "the form view to use
for details"
. No layer from spec to console ever resolved a view by that name. Its one read in
the shipped console passed the value into the second argument of onNavigate — the slot
that otherwise carries the navigation-MODE token — so an authored name did not select a view,
it substituted for the mode. A consumer in the same bundle reads that argument against a
closed two-value vocabulary (edit / view), so any other authored value matched neither
branch. Declared, consumed, and wrong.

Acceptance criteria — executable, with both controls

PROBEnavigation: { view: 'summary_view' } is now refused, at the key the author wrote,
with the prescription naming page assignment as the route:

PROBE  navigation:{view:"summary_view"} -> success = false
       path = navigation.view
       message = `view.list.navigation.view` was removed in @objectstack/spec 17.5.0
                 (ADR-0049 enforce-or-remove) — ... Delete the key; to choose what opens for a
                 record, assign a `record` page to the object and let `isDefault` pick the one
                 that opens — page assignment is the machinery that resolves a detail layout ...
LIT    navigation:{mode:"page"}         -> success = true
LIT    all five surviving keys          -> success = true

LIT CONTROL — the live siblings still parse. { mode: 'page' } is accepted, and so are all
five survivors together (mode, preventNavigation, openNewTab, size, width). A tombstone
that broke its siblings would satisfy the refusal assertion while being a larger bug, and
navigation is one closed shape, so that blast radius is the whole block. Pinned at all three
doors — ListViewSchema, ObjectListViewSchema, and the flattened PUT /api/v1/meta/view
overlay.

DARK CONTROL — the reading used is the regenerated packages/spec/authorable-surface/ui.json
ui/NavigationConfig:* row set. It returned: 6 rows before, 6 rows after; exactly one line
changed, ui/NavigationConfig:view -> ui/NavigationConfig:view [RETIRED]; the retired key
never appears plain, and no other ui/NavigationConfig:* row disappeared.

     "ui/NavigationConfig:openNewTab",
     "ui/NavigationConfig:preventNavigation",
     "ui/NavigationConfig:size",
-    "ui/NavigationConfig:view",
+    "ui/NavigationConfig:view [RETIRED]",
     "ui/NavigationConfig:width",

Gates observed RED before GREEN

A gate never observed failing for this change is not known to be a gate for it.

gate red green
check:generated (check:docs leg) ✗ 1 of 15 artifact(s) stale: content/docs/references/** ✓ All 15 generated artifacts are up to date. after --fix regenerated exactly that one
check:generated (check:api-surface leg) ✗ ... dist/**/*.d.ts describe DIFFERENT sources than the ones on disk (stale-dist trap, after the test file moved) after a real rebuild
check-adr-0087-registration exit 1declares a breaking change (BREAKING) but no adr-0087: disposition marker — proven by committing the changeset with the marker line dropped exit 01 declared-breaking changeset(s), each carrying an ADR-0087 disposition ... registered list-view-navigation-view-retired
the three refusal pins 3 failed / 8 passed under an ablation that restored the pre-retirement live string 11 passed on the real tree

The ablation mutated packages/spec/src/ui/view.zod.ts on disk (tombstone occurrences 1 -> 0,
ablation marker 0 -> 1, blob hash moved), ran the pins, then restored under a trap — restored
blob hash byte-identical to HEAD and git diff HEAD empty. Note the direction: only the three
refusal pins went red; the eight sibling-acceptance assertions stayed green, which is the correct
shape for reverting a tombstone.

Gate results

pnpm --filter @objectstack/spec check:generatedall 15 green, including the four named on
the card: check:authorable-surface, check:api-surface, check:docs, check:liveness. Working
tree clean afterwards (no regeneration drift).

pnpm --filter @objectstack/spec typecheck — green. pnpm --filter @objectstack/spec test
484 files, 13830 tests, all passed. pnpm lint (repo-wide, eslint . --no-inline-config) —
green, so no narrowing was taken and none is declared.

All 14 source audits check:generated deliberately does not run — green.
check:skill-examples first reported PREREQUISITE NOT MET (packages/client-react/dist unbuilt
in a fresh worktree); after building that closure it reads ✅ 258 prose examples type-check across 3 surface(s).

node scripts/check-changeset-no-major.mjs — green, no major bump.

check-widening-tells did not recur. node scripts/pm/check-widening-tells.mjs --declaration no --diff <this diff> exits 0: 10 changed file(s) — 4 judged against a declared surface (no widening tell), 6 NOT MEASURED. The T1/T2 firing on retiredKey() lines recorded on #17955 and
#17300 did not reproduce here, so there is nothing to report as a regression and the
declaration is unchanged, as ruled.

The ADR-0087 disposition is SEMANTIC, deliberately

The ruling asked for a semantic migration entry, and the shape earns it: a mechanical D2 strip
would delete the key without recording which list view lost it, and an author who wrote it
wanted a named detail layout — a want page assignment serves and a stripped key does not record.
So the entry is a D3 SemanticMigration, list-view-navigation-view-retired, and the tombstone
prescription therefore carries no os migrate meta sentence: that sentence is owed only where
a conversion covers the surface (shared/retired-key.ts module docblock; the class pin
retired-key-migrate-sentence.test.ts deliberately judges nothing when the marker is absent).
The precedent for registering a retired key with no D2 conversion is
data/AggregationNode:distinct in this same table.

Registered as ui/NavigationConfig:view in RETIRED_KEYS_BY_MAJOR[18], which also starts its
aging clock. Note for the reviewer: no major-18 semantic entry reaches spec-changes.json or
docs/protocol-upgrade-guide.md yet
— measured, not assumed: four sampled major-18 ids
(change-management-family-retired, training-family-retired, scim-provider-object-retired,
epoch-instant-keys-renamed) return 0 in both files, while major-17 ids return non-zero and the
guide stops at "Protocol 17". This entry behaving the same way is the steady state for the open
window, not a gap.

Liveness ledger — nothing to update, and why

view/list/navigation is one live row with no classified children, because check-liveness
drills one level. That is #17424's subject; it is cited here and not fixed, exactly as ruling
item 4 directs. Since the walk never reaches navigation's children, no ledger row exists for
view to mark dead, and none is owed — check:liveness is green with the tombstone in place,
with no UNCLASSIFIED row. packages/spec/liveness/** is untouched (held by #18582).

The objectui contract twin is in the SIBLING repo — reported, not touched

Ruling item 3 asked where the contract twins live. They are in objectui, not here. The
reading:

  • objectui/packages/types/src/__tests__/view-navigation-config-spec-parity.test.ts at the pinned
    .objectui-sha (53ded82bf7a494f54e344e19099dbf00854b8694) is the twin. It asserts
    Equal<ViewNavigationConfig, NavigationConfig>ViewNavigationConfig is this spec type,
    re-exported by reference since objectui#4588.
  • That file authors the retired key as a legal value in four places, e.g.
    const navigation: ViewNavigationConfig = { view: 'summary_view' };. Once objectui resolves a
    spec carrying this tombstone, those lines become tsc errors under that package's type-check.
  • In this repo there is no twin pin covering the key: repo-wide, ViewNavigationConfig occurs
    only in packages/console/CHANGELOG.md (a release-owned file), and the tracked
    sdui.manifest.json declares navigation as a flat object input with no drill into view,
    so check:react-declaration-parity is structurally blind to it. Nothing here to update.

The Console Pin Gate is not affected and does not run on this PR. Measured: that job is gated
on the console paths filter (.objectui-sha plus four scripts), none of which this diff touches;
and it runs scripts/build-console.sh — a build, which does not type-check the sibling's
tsconfig.test.json. objectui's src at the pinned SHA contains no value write of
navigation.view outside that test file (only docblocks), and reads of .view still compile
against never | undefined. So this PR does not turn main red.

What is still owed, in the other repository: the twin gains the tombstone pin before objectui
picks up a spec carrying this retirement. Its dependency is @objectstack/spec: ^17.0.0, so a
published 17.5.0 is in range.

File surface

Every path touched, including the pre-declared OPEN set.

path why
packages/spec/src/ui/view.zod.ts the retiredKey() tombstone + its prescription constant
packages/spec/src/migrations/entries/retired-keys/18.ui__NavigationConfig__view.ts new — RETIRED_KEYS_BY_MAJOR[18] registration (build-schemas gate (b))
packages/spec/src/migrations/entries/semantic/18.list-view-navigation-view-retired.ts new — the ADR-0087 D3 semantic entry
packages/spec/src/ui/view.test.ts the pins: refusal at three doors, plus the lit controls
packages/spec/src/migrations/registry.ts OPEN set — generated by gen:migration-registry, never hand-edited
.changeset/16885-retire-navigation-view.md OPEN set — minor, **BREAKING** banner, FROM -> TO, ADR-0087 disposition
packages/spec/authorable-surface/ui.json OPEN set — regenerated by build
content/docs/references/ui/view.mdx OPEN set — regenerated by gen:docs
content/docs/references/api/protocol.mdx OPEN set — regenerated by gen:docs (the inline navigation summary drops view?: string)
content/docs/references/data/object.mdx OPEN set — same, one row

Held by other in-flight work and not touched: packages/spec/scripts/build-schemas.ts (#17969),
packages/spec/liveness/** and packages/spec/scripts/liveness/check-liveness.mts (#18582),
packages/spec/src/api/protocol.zod.ts (#18597). Ruling item 2 was already discharged — PR #17796
reads closed, not merged — so the retirement is written fresh.

Acceptance notes

Noted, not filed: nothing in the touched files.

One finding to file separately, unrelated to this diff and not fixed here:
pnpm check:cross-package-test-inputs reaches opposite verdicts on an identical source tree
depending only on whether packages/spec/dist/ happens to be built — exit 1 with it present
(115 entries), exit 0 with the same directory moved aside, same commit, restored afterwards.
The flagged radius is packages/spec/dist/ descended from
packages/cli/test/init-created-files-summary.e2e.test.ts, which this diff does not touch. No
pnpm build step precedes pnpm check:cross-package-test-inputs in lint.yml's lint job, so
CI always runs it against an unbuilt tree and it passes vacuously — the declaration hole it exists
to catch is structurally invisible on every PR. That is the "a verifier that silently degrades is
worse than no verifier" shape from AGENTS.md Route & surface ownership rule 3.

维护者速读(草稿)

改了什么 —— ListViewSchema.navigation.view 退役:键仍在 shape 里,但变成 ADR-0049 墓碑,
写它是 tsc 报错、传值进来是带处方的 parse 拒收。处方点名替代路线:给对象发布 record 页面,
isDefault 决定打开哪一个。同批落地 ADR-0087 的 D3 语义迁移条目、changeset(minor +
**BREAKING**)、以及三道门上的拒收 pin。

为什么改 —— 这个键承诺"用哪个表单视图打开详情",而从 spec 到 console 没有任何一层按名字解析
视图。它唯一的读取把值塞进了 onNavigate第二个参数 —— 那个槽位装的是导航模式令牌。
所以作者写的视图名不是在选视图,而是在顶替模式;而同一个 bundle 里有消费者拿这个参数去对
edit/view 两值词表,任何别的值两个分支都不匹配。仓库内零编写实例,唯一的外部作者已自行删除。
维护者 2026-09-13 裁决 B:零拉力即移除。

风险与代价(含回滚) —— 风险低。行为面没有回归可言:这个键从来没选中过任何东西,决定详情怎么
呈现的是 modesize,两者一字未动,并且有 pin 钉着五个存活键仍然接受。代价是发布面:这是
breaking,升级时仍写着该键的元数据会在三道门(含 PUT /api/v1/meta/view)被拒。回滚就是
revert 本 PR —— 墓碑、注册条目、changeset 与生成产物都在同一个提交序列里,没有跨仓副作用。
一个待办留在姊妹仓:objectui 的契约孪生 pin 把 { view: 'summary_view' } 写成合法值,需要在
objectui 取到带墓碑的 spec 之前补上墓碑 pin。本 PR 不触发 Console Pin Gate,不会让 main 变红。

席位意见 ——

你要做的 ——


Generated by Claude Code

WIP: tombstone + ADR-0087 registration. Generated artifacts follow.

Claude-Session: https://claude.ai/code/session_01JbZnqu8bt6YqfJsr9vaFb3
Co-authored-by: Claude <noreply@anthropic.com>
Tombstone, ADR-0087 registration, regenerated artifacts and the pins.

Claude-Session: https://claude.ai/code/session_01JbZnqu8bt6YqfJsr9vaFb3
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions github-actions Bot added size/m documentation Improvements or additions to documentation protocol:ui tests tooling labels Sep 17, 2026
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/spec, touching 8 documentable anchor(s). ⚠️ 2 changed file(s) yielded no anchor (packages/spec/authorable-surface/ui.json, packages/spec/src/migrations/entries/retired-keys/18.ui__NavigationConfig__view.ts), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files.

6 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:

  • content/docs/api/plugin-endpoints.mdx (via /api/v1/meta/view (route, a path literal in acceptanceCriteria; a path literal in semantic))
  • content/docs/concepts/metadata-lifecycle.mdx (via /api/v1/meta/view (route, a path literal in acceptanceCriteria; a path literal in semantic))
  • content/docs/kernel/services-checklist.mdx (via /api/v1/meta/view (route, a path literal in acceptanceCriteria; a path literal in semantic))
  • content/docs/protocol/objectui/index.mdx (via /api/v1/meta/view (route, a path literal in acceptanceCriteria; a path literal in semantic))
  • content/docs/protocol/objectui/layout-dsl.mdx (via summary_view (literal, a string literal in NavigationConfigSchema))
  • content/docs/ui/forms.mdx (via /api/v1/meta/view (route, a path literal in acceptanceCriteria; a path literal in semantic))
What this run could not see
  • 2 changed file(s) yielded no anchor (packages/spec/authorable-surface/ui.json, packages/spec/src/migrations/entries/retired-keys/18.ui__NavigationConfig__view.ts) — pages documenting those are invisible to this run
  • 4 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 60 of 215 client-bound route-ledger rows — the other 155 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 155: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 55 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 100 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.

Coarse fallback — 136 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json 21b7c12b4f779b86409db2a7bb9235fe80ef8a54packageMentionDocs.

Which tree this was computed on

This run read content/docs from a693aa4e1bac6118d3280c0d95fc8fa996c4593c — the merge of head 650c7938a962900518041f8aac7279e056166367 into base 21b7c12b4f779b86409db2a7bb9235fe80ef8a54, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin a693aa4e1bac6118d3280c0d95fc8fa996c4593c && git checkout a693aa4e1bac6118d3280c0d95fc8fa996c4593c
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 21b7c12b4f779b86409db2a7bb9235fe80ef8a54 650c7938a962900518041f8aac7279e056166367 && git checkout -B drift-repro 21b7c12b4f779b86409db2a7bb9235fe80ef8a54 && git merge --no-ff 650c7938a962900518041f8aac7279e056166367

node scripts/docs-audit/affected-docs.mjs --json 21b7c12b4f779b86409db2a7bb9235fe80ef8a54

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs 21b7c12b4f779b86409db2a7bb9235fe80ef8a54 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@os-bill os-bill added domain:spec priority:p2 Medium: important, M3 labels Sep 17, 2026 — with Claude
@os-bill
os-bill marked this pull request as ready for review September 17, 2026 09:38
@os-bill
os-bill added this pull request to the merge queue Sep 17, 2026
Merged via the queue into main with commit 3d8779d Sep 17, 2026
45 checks passed
@os-bill
os-bill deleted the claude/issue-16885-retire-navigation-view branch September 17, 2026 10:06
os-tesla pushed a commit to objectstack-ai/objectui that referenced this pull request Sep 20, 2026
… the published README (objectui#9987)

The README's record-navigation paragraph told an author to write the
member that `@objectstack/spec` retired under ADR-0049
(objectstack-ai/objectstack#18619) — and then, in the same paragraph,
explained the mechanism by which an undeclared key rejects the whole
config. The first sentence caused what the second diagnosed. This file
is listed in `files[]`, so the instruction shipped in the npm tarball.

The instruction is replaced by the route the retirement actually leaves
standing, carried in from `@object-ui/react`'s `useNavigationOverlay`
docblock: assign a `record` page to the object and let `isDefault` pick
the one that opens. Deleting the sentence alone was not enough — it
answered a real authoring question, and a reader who loses the answer
goes looking and finds the retired key elsewhere.

The second sentence is kept byte-identical (modulo re-wrapping): it is
the only place this README tells an author why a typo costs them the
whole block. It just stops being a description of what the paragraph
above it told them to do.

`readme-navigation-example.test.ts` now measures the PROSE as well as
the fence. The fence tests could never have caught this — they extract
the ```json block, and the block never carried the key. The pinned
`@objectstack/spec@17.4.0` still declares `view`, so nothing
schema-derived can say it is retired in this tree; it is named by hand,
exactly as `basePath` already is, and the absence carries its own
control: the same detector is run over the sentence that used to carry
the instruction and must find it there.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018HrVaotisyhgmot9o2MLRq
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation domain:spec priority:p2 Medium: important, M3 protocol:ui size/m tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

List view navigation.view is declared in spec but resolves no form view — its only read lands it in the onNavigate navigation-MODE argument

2 participants